/*
 * Copyright (C) 2009 M. Hautle.
 * 
 * This file is part of TrackedBean.
 * 
 * TrackedBean is free software: you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free
 * Software Foundation, either version 3 of the License, or (at your option) any later version.
 * 
 * TrackedBean is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
 * A PARTICULAR PURPOSE. See the GNU Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public License along with TrackedBean. If not, see <http://www.gnu.org/licenses/>.
 */
package ch.trackedbean.util;

import java.beans.*;
import java.lang.reflect.*;
import java.util.*;
import ch.trackedbean.common.*;

/**
 * Helper evaluating simple EL expressions on a beans.<br>
 * Currently the following features were supported (also in combination):<br>
 * - Nested properties (<code>address.city.code</code>)<br>
 * - Array/listelements (<code>address[2]</code>)<br>
 * - Mapelements identified by their key (<code>members['john']</code>)<br>
 * 
 * @author M. Hautle
 */
public class ELHelper {
	/** Mask for an '.' control character. */
	private static final int POINT = 0x1;

	/** Mask for an '[' control character. */
	private static final int OPEN_BRACET = 0x2;

	/** Mask for an ']' control character. */
	private static final int CLOSING_BRACET = 0x4;

	/** Object representing an undefined value. */
	private static final Object UNDEFINED = new Object();

	/**
	 * Hidden constructor.
	 */
	private ELHelper() {
	}

	/**
	 * Returns the value of the passed expression.<br>
	 * <code>null</code> is returned if the value is undefined.
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed
	 * @return The value of the passed expression on the given bean or null
	 * @throws TechnicalException If something went wrong
	 */
	public static Object getValueOf(String el, Object bean) throws TechnicalException {
		return getValueOf(el, bean, null);
	}

	/**
	 * Returns the value of the passed expression.<br>
	 * <code>null</code> is returned if the value is undefined.
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed
	 * @param checked True if the passed EL expression should be checked using {@link #isValid(String)}
	 * @return The value of the passed expression on the given bean or null
	 * @throws TechnicalException If something went wrong
	 */
	public static Object getValueOf(String el, Object bean, boolean checked) throws TechnicalException {
		return getValueOf(el, bean, null, checked);
	}

	/**
	 * Returns the value of the passed expression.
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed
	 * @param undefined The object to return if the result is undefined
	 * @return The value of the passed expression on the given bean
	 * @throws TechnicalException If something went wrong
	 */
	public static Object getValueOf(String el, Object bean, Object undefined) throws TechnicalException {
		return getValueOf(el, bean, undefined, true);
	}

	/**
	 * Returns the value of the passed expression.
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed
	 * @param undefined The object to return if the result is undefined
	 * @param checked True if the passed EL expression should be checked using {@link #isValid(String)}
	 * @return The value of the passed expression on the given bean
	 * @throws TechnicalException If something went wrong
	 */
	public static Object getValueOf(String el, Object bean, Object undefined, boolean checked) throws TechnicalException {
		if (checked)
			checkEL(el);
		final Object value = getValueOfInternal(el, bean);
		return value == UNDEFINED ? undefined : value;
	}

	/**
	 * Returns the value of the passed expression. <br>{@link #UNDEFINED} is returned if the expression couldn't be evaluated (cause there were somwhere null
	 * values)
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed
	 * @return The value of the passed expression on the given bean or {@link #UNDEFINED}
	 * @throws TechnicalException If something went wrong
	 */
	private static Object getValueOfInternal(String el, Object bean) throws TechnicalException {
		// flag indicating if the current 'atom' is an simple property
		boolean prop = true;
		for (int i = 0, end = el.length() - 1; bean != null && i < end;) {
			final String ex = el.substring(i, i = getEndIndex(el, i));
			// call the corresponding resolver (simple property/'indexed' property)
			bean = prop ? getPropertyValue(ex, bean) : getIndexedValue(ex, bean);
			// last element?
			if (i >= end)
				return bean;
			// if the end index is a . then the next atom will be a property
			// skip following '.' (may occour after a index -> a[2].b and in this case the following element is a property)
			if (prop = el.charAt(i) == '.' || el.charAt(++i) == '.')
				i++;
		}
		return UNDEFINED;
	}

	/**
	 * Sets the value on the passed bean using the expression to figure out the right location.
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed or null
	 * @param value The value to set (may be null)
	 * @return True if something was copied
	 * @throws TechnicalException If something went wrong
	 */
	public static boolean setValueOf(String el, Object bean, Object value) throws TechnicalException {
		return setValueOf(el, bean, value, true);
	}

	/**
	 * Sets the value on the passed bean using the expression to figure out the right location.
	 * 
	 * @param el The expression
	 * @param bean The bean on which the expression gets executed or null
	 * @param value The value to set (may be null)
	 * @param checked True if the passed EL expression should be checked using {@link #isValid(String)}
	 * @return True if something was copied
	 * @throws TechnicalException If something went wrong
	 */
	public static boolean setValueOf(String el, Object bean, Object value, boolean checked) throws TechnicalException {
		if (checked)
			ELHelper.checkEL(el);
		// flag indicating if the current 'atom' is an simple property
		boolean prop = true;
		for (int i = 0, cnt = el.length() - 1; bean != null && i < cnt;) {
			final String ex = el.substring(i, i = getEndIndex(el, i));
			// if this is the last expression part we write to it instead of reading it
			if (i >= cnt)
				return setValue(ex, bean, value);
			// call the corresponding resolver (simple property/'indexed' property)
			bean = prop ? getPropertyValue(ex, bean) : getIndexedValue(ex, bean);
			// if the end index is a . then the next atom will be a property
			// skip following '.' (may occour after a index -> a[2].b and in this case the following element is a property)
			if (prop = el.charAt(i) == '.' || el.charAt(++i) == '.')
				i++;
		}
		return false;
	}

	/**
	 * Returns the name of the last property in the given expression.
	 * 
	 * @param el The expression
	 * @return The last part of the expression
	 */
	public static String getLastProperty(String el) {
		final int index = el.lastIndexOf('.');
		if (index < 1)
			return el;
		return el.substring(index + 1);
	}

	/**
	 * Checks wherever the given expression is could be replaced with at {@link PropertyDescriptor}.
	 * 
	 * @param el The expression
	 * @return True if it's just a flatt property access
	 */
	public static boolean isSimpleProperty(String el) {
		return getEndIndex(el, 0) == el.length();
	}

	/**
	 * Returns the value.
	 * 
	 * @param identifier The the index or the map key
	 * @param element The element on which the operation should be executed
	 * @return The resulting value
	 * @throws TechnicalException If something goes wrong (during reflection etc.)
	 */
	static Object getIndexedValue(String identifier, Object element) throws TechnicalException {
		// list
		if (element instanceof List)
			return ((List) element).get(Integer.parseInt(identifier));
		// map
		if (element instanceof Map) {
			final char c = identifier.charAt(0);
			if (c == '\'' || c == '"')
				identifier = identifier.substring(1, identifier.length() - 1);
			return ((Map) element).get(identifier);
		}
		// array
		if (element instanceof Object[])
			return ((Object[]) element)[Integer.parseInt(identifier)];
		return null;
	}

	/**
	 * Returns the value.
	 * 
	 * @param identifier The property name
	 * @param element The element on which the operation should be executed
	 * @return The resulting value
	 * @throws TechnicalException If something goes wrong (during reflection etc.)
	 */
	static Object getPropertyValue(String identifier, Object element) throws TechnicalException {
		final Class<? extends Object> type = element.getClass();
		try {
			final Method read = BeanUtils.getGetter(type, identifier);
			if (read != null)
				return read.invoke(element);
		} catch (Exception e) {
			throw new TechnicalException("Getting the value " + identifier + " from " + type.getName() + " failed!", e);
		}
		return null;
	}

	/**
	 * Sets the value.
	 * 
	 * @param identifier The property name, the index or the map key
	 * @param element The element on which the operation should be executed
	 * @param value The value to set
	 * @return True if the value was set
	 * @throws TechnicalException If something goes wrong (during reflection etc.)
	 */
	@SuppressWarnings("unchecked")
	static boolean setValue(String identifier, Object element, Object value) throws TechnicalException {
		// list
		if (element instanceof List) {
			((List) element).set(Integer.parseInt(identifier), value);
			return true;
		}
		// map
		if (element instanceof Map) {
			final char c = identifier.charAt(0);
			if (c == '\'' || c == '"')
				identifier = identifier.substring(1, identifier.length() - 1);
			((Map) element).put(identifier, value);
			return true;
		}
		// array
		final Class<? extends Object> type = element.getClass();
		if (type.isArray()) {
			((Object[]) element)[Integer.parseInt(identifier)] = value;
			return true;
		}
		// bean
		try {
			final Method write = BeanUtils.getSetter(type, identifier);
			if (write == null)
				return false;
			write.invoke(element, value);
		} catch (Exception e) {
			throw new TechnicalException("Setting the value " + identifier + " from " + type.getName() + " failed!", e);
		}
		return true;
	}

	/**
	 * Returns the end index (exclusive) of the expression starting at <code>index</code>.
	 * 
	 * @param el The complete expression
	 * @param index The starting index (inclusive)
	 * @return The end index (exclusive) of the expression starting at <code>index</code>
	 */
	static int getEndIndex(String el, int index) {
		final int cnt = el.length();
		for (int i = index; i < cnt; i++) {
			switch (el.charAt(i)) {
				case '.':
				case '[':
				case ']':
					return i;
					// skip strings
				case '\'':
				case '"':
					final char c = el.charAt(i);
					for (i++; i < cnt && el.charAt(i) != c; i++)
						;
			}
		}
		return cnt;
	}

	/**
	 * Ensures that the given expression is valid, if not a {@link IllegalArgumentException} will be thrown.
	 * 
	 * @param el The expression to check
	 * @throws IllegalArgumentException If the EL expression is not valid.
	 */
	public static void checkEL(String el) throws IllegalArgumentException {
		if (!isValid(el))
			throw new IllegalArgumentException("\"" + el + "\" is not a valid input sequence!");
	}

	/**
	 * Checks the given expression on syntactical correctness.
	 * 
	 * @param el The expression to check
	 * @return True if the expression is valid
	 */
	public static boolean isValid(String el) {
		// a el expression must start with an identifier part
		if (el.length() == 0 || !Character.isJavaIdentifierPart(el.charAt(0)))
			return false;
		boolean valid = true;
		// mask defining the next allowed control character(s)
		int mask = POINT | OPEN_BRACET;
		// check the expression
		for (int i = 1, cnt = el.length(); valid && i < cnt; i++) {
			final char c = el.charAt(i);
			switch (c) {
				case '.':
					valid = (mask & POINT) == POINT;
					mask = POINT | OPEN_BRACET;
					break;
				case '[':
					valid = (mask & OPEN_BRACET) == OPEN_BRACET;
					mask = CLOSING_BRACET;
					break;
				case ']':
					valid = (mask & CLOSING_BRACET) == CLOSING_BRACET;
					mask = POINT | OPEN_BRACET;
					break;
				// skip strings
				case '\'':
				case '"':
					for (i++; i < cnt && el.charAt(i) != c; i++)
						;
					break;
				default:
					valid = Character.isJavaIdentifierPart(c);
			}
		}
		return valid;
	}
}
